<?php
/**
 * Slipstream :: Highly adaptive dependency injection framework
 * Copyright 2010-2011 Ian Unruh (Xerof3ar)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
namespace Slipstream\Configuration;

use ArrayObject;
use InvalidArgumentException;
use SplObjectStorage;

use Slipstream\Configuration\Option\OptionInterface;
use Slipstream\Configuration\Option\PredicatedOption;

use Terrafirma\Predicate\InstanceOfPredicate;
use Terrafirma\Predicate\Type\BoolPredicate;

/**
 * Abstract implementation of PolicyInterface
 *
 * @author Ian Unruh (Xerof3ar) <ianunruh@gmail.com>
 * @since Class avaliable since release 1.0
 */
abstract class AbstractPolicy implements PolicyInterface
{
	// {{{ Policy options
	/**
	 * @desc Should matching policies be disabled?
	 * @type bool
	 * @default false
	 */
	const OPTION_DISABLED = 'disabled';
	
	/**
	 * @desc The scope of the context tree affected by this policy
	 * @type PolicyScope
	 * @default PolicyScope::InheritByChildren
	 */
	const OPTION_SCOPE = 'scope';
	// }}} Policy options
	
	/**
	 * @var ArrayObject<string:OptionInterface>
	 */
	protected $options;
	
	/**
	 * Class constructor
	 *
	 * @param Traversable<string:mixed> $options [optional]
	 * @return void
	 */
	public function __construct($options = null)
	{
		$this->options = new ArrayObject();
		$this->setupOptions();
		
		if(!is_null($options)) {
			$this->setAll($options);
		}
	}
	
	/**
	 * Returns the value of a named option
	 *
	 * @throws InvalidArgumentException If option is undefined
	 * @param string $name
	 * @return mixed
	 */
	public function get($name)
	{
		return $this->getOption($name)->getValue();
	}
	
	/**
	 * Sets the value of a named option
	 *
	 * @throws InvalidArgumentException If option is undefined
	 * @throws InvalidArgumentException If value of option is invalid
	 * @param string $name
	 * @param mixed $value
	 * @return void
	 */
	public function set($name, $value)
	{
		$option = $this->getOption($name);
		$option->setValue($value);
	}
	
	/**
	 * Sets the values of multiple options using a map
	 *
	 * @param Traversable<string:mixed> $options
	 * @return void
	 */
	public function setAll($options)
	{
		foreach($options as $name => $value) {
			$this->set($name, $value);
		}
	}
	
	/**
	 * Returns true if this policy matches another policy
	 *
	 * @param PolicyInterface $policy
	 * @return bool
	 */
	public function matches(PolicyInterface $otherPolicy)
	{
		if(!($otherPolicy instanceof $this)) {
			return false;
		}
		
		$options = $this->getOptionsToCompare();
		foreach($options as $option) {
			if($this->get($option) !== $otherPolicy->get($option)) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Merges a set of policies with this policy to create a single policy
	 *
	 * @param ArrayObject<PolicyInterface> $policies
	 * @return PolicyInterface
	 */
	public function merge(ArrayObject $policies)
	{
		$mergedOptions = new ArrayObject();
		
		$policies = $policies->getArrayCopy();
		array_unshift($policies, $this);
		
		foreach($policies as $policy) {
			$options = $policy->getOptions();
			foreach($options as $option) {
				// Options that were set always take precendence
				if($option->isChanged()) {
					$mergedOptions->offsetSet($option->getName(), $option->getValue());
				}
			}
		}
		
		return new static($mergedOptions);
	}
	
	/**
	 * Returns an option by its name
	 *
	 * @param string $name
	 * @return OptionInterface
	 */
	public function getOption($name)
	{
		if(!$this->options->offsetExists($name)) {
			throw new InvalidArgumentException(sprintf('Option "%s" not defined', $name));
		}
		return $this->options->offsetGet($name);
	}
	
	/**
	 * Returns the options defined in this policy
	 * @return ArrayObject<OptionInterface>
	 */
	public function getOptions()
	{
		return $this->options;
	}
	
	/**
	 * Called in the constructor to setup the options and their default values
	 * @return void
	 */
	protected function setupOptions()
	{
		$this->addOption(new PredicatedOption(
			self::OPTION_DISABLED,
			new BoolPredicate(),
			false));
		
		$this->addOption(new PredicatedOption(
			self::OPTION_SCOPE,
			new InstanceOfPredicate('Slipstream\Configuration\PolicyScope'),
			PolicyScope::InheritByChildren()));
	}
	
	/**
	 * Returns the names of options used to match policies to each other
	 * @return Traversable<string>
	 */
	protected function getOptionsToCompare()
	{
		return array();
	}
	
	/**
	 * Adds an option to the policy
	 *
	 * @param OptionInterface $option
	 * @return void
	 */
	protected function addOption(OptionInterface $option)
	{
		$this->options->offsetSet($option->getName(), $option);
	}
	
	/**
	 * Creates and adds an option based off of given arguments
	 *
	 * @param string $name
	 * @param mixed $value [optional]
	 * @param PredicateInterface $predicate [optional]
	 * @return OptionInterface The newly created option
	 */
	protected function createOption($name, $value = null, PredicateInterface $predicate = null)
	{
		if(is_null($predicate)) {
			$option = new SimpleOption($name, $value);
		} else {
			$option = new PredicatedOption($name, $value, $predicate);
		}
		
		$this->options->offsetSet($name, $option);
		
		return $option;
	}
}